%%%-------------------------------------------------------------------
%%% @author Viktor Green <viktor@student.gu.se>
%%% @copyright (C) 2011, Viktor Green
%%% @doc
%%% Module to handle file reading and writing.
%%% Implemented as a gen_server to avoid conflicts. The server handles
%%% only one read or write operation at a time.
%%% At the moment it is uncertain if it would be best to implement the
%%% write method from handle_call or from handle_cast.
%%%
%%% If I would put it as cast it could "que up" writes if they take
%%% time, and the client wouldn't be affected, but in that way we
%%% wouldn't know if the data was written successfulley. 
%%% @end
%%% Created :  7 Dec 2011 by Viktor Green <viktor@student.gu.se>
%%%-------------------------------------------------------------------
-module(file_handler).

-behaviour(gen_server).

%% API
-export([start_link/1, write/3, allocate/2, close/1, read/3]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
	 terminate/2, code_change/3]).

-export([allocate/3]).

-define(SERVER, ?MODULE). 

-record(state, {device :: file:io_device()
	       ,allocating = false :: boolean()}).

%%%===================================================================
%%% API
%%%===================================================================

%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%%
%% @spec start_link(FileName) -> {ok, Pid} | ignore | {error, Error}
%% @end
%%--------------------------------------------------------------------
start_link(FileName) ->
    gen_server:start_link(?MODULE, FileName, []).


%%--------------------------------------------------------------------
%% @doc
%% API call to read Length number of bytes at Position (bytes from the
%% beginning of the file) in the file.
%% @end
%%--------------------------------------------------------------------
-spec read(Pid :: pid(), Position :: integer(), Length :: integer()) ->
		  Data :: binary().
read(Pid, Position, Length) ->
    gen_server:call(Pid, {read, Position, Length}).

%%--------------------------------------------------------------------
%% @doc
%% API call to write the provided Data at Position (bytes from the
%% beginning of the file) in the file.
%% @end
%%--------------------------------------------------------------------
-spec write(Pid :: pid(), Position :: integer(),
	    Data :: string() | binary()) -> ok.
write(Pid, Position, Data) ->
    gen_server:cast(Pid, {write, Position, Data}).

%%--------------------------------------------------------------------
%% @doc
%% API call to allocate a certain amount of bytes in the file
%% @end
%%--------------------------------------------------------------------
-spec allocate(Pid :: pid(), Size :: integer()) -> ok.
allocate(Pid, Size) ->
    gen_server:call(Pid, {allocate, Size}).

%%--------------------------------------------------------------------
%% @doc
%% API call to close the file and terminate the gen_server
%% @end
%%--------------------------------------------------------------------
-spec close(Pid :: pid()) -> ok.
close(Pid) ->
    gen_server:call(Pid, close).


%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%%
%% @spec init(Args) -> {ok, State} |
%%                     {ok, State, Timeout} |
%%                     ignore |
%%                     {stop, Reason}
%% @end
%%--------------------------------------------------------------------
init(FileName) ->
    FileOptions = [read, write, binary, raw, read_ahead,
		   {delayed_write, 1024 * 1024, 2000}],
    case file:open(FileName, FileOptions) of
	{ok, Handle} -> {ok, #state{device = Handle}};
	{error, Reason} -> {stop, Reason}
    end.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%%
%% @spec handle_call(Request, From, State) ->
%%                                   {reply, Reply, State} |
%%                                   {reply, Reply, State, Timeout} |
%%                                   {noreply, State} |
%%                                   {noreply, State, Timeout} |
%%                                   {stop, Reason, Reply, State} |
%%                                   {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_call({read, Position, Size},
	    _From,
	    #state{device = Device} = State) ->
    {ok, Data} = file:pread(Device, Position, Size),
    {reply, Data, State};
handle_call({allocate, Size},
	    _From,
	    #state{device = Device} = State) ->
    spawn(?MODULE, allocate, [Device, Size, []]),
    {reply, ok, State};
handle_call(close, _From, State) ->
    {stop, normal, closing, State};
handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @spec handle_cast(Msg, State) -> {noreply, State} |
%%                                  {noreply, State, Timeout} |
%%                                  {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_cast({write, Position, Data},
	    #state{device = Device} = State) ->
    ok = file:pwrite(Device, Position, Data),
    {noreply, State};             
handle_cast(_Msg, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%%                                   {noreply, State, Timeout} |
%%                                   {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_info(_Info, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
terminate(_Reason, #state{device = Device}) ->
    file:close(Device).

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================


%%--------------------------------------------------------------------
%% @doc
%% Function to allocate a specified amount of bytes in the opened file
%% @end
%%--------------------------------------------------------------------
-spec allocate(Device :: file:io_device(), Size :: integer(),
	       Binary :: iolist()) ->
		      ok | {error, Reason :: term()}.
allocate(Device, 0, Binary) ->
    file:write(Device, Binary);
allocate(Device, SizeLeft, Binary) ->
    allocate(Device, SizeLeft - 1, [<<0:8>> | Binary]).


